//	VirtualDub - Video processing and capture application
//	A/V interface library
//	Copyright (C) 1998-2004 Avery Lee
//
//	This program is free software; you can redistribute it and/or modify
//	it under the terms of the GNU General Public License as published by
//	the Free Software Foundation; either version 2 of the License, or
//	(at your option) any later version.
//
//	This program is distributed in the hope that it will be useful,
//	but WITHOUT ANY WARRANTY; without even the implied warranty of
//	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//	GNU General Public License for more details.
//
//	You should have received a copy of the GNU General Public License
//	along with this program; if not, write to the Free Software
//	Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

#ifdef _MSC_VER
	#pragma warning(disable: 4786)		// STFU
#endif

#include <vd2/Riza/capdriver.h>
#include <vd2/system/vdstring.h>
#include <vd2/system/error.h>
#include <vd2/system/time.h>
#include <windows.h>
#include <vfw.h>
#include <vector>

using namespace nsVDCapture;

extern HINSTANCE g_hInst;

static const CAPTUREPARMS g_defaultCaptureParms={
	1000000/15,		// 15fps
	FALSE,			// fMakeUserHitOKForCapture
	100,			// wPercentDropForError
	TRUE,			// callbacks won't work if Yield is TRUE
	324000,			// we like index entries
	4,				// wChunkGranularity
	FALSE,			// fUsingDOSMemory
	10,				// wNumVideoRequested
	TRUE,			// fCaptureAudio
	0,				// wNumAudioRequested
	0,				// vKeyAbort
	FALSE,			// fAbortLeftMouse
	FALSE,			// fAbortRightMouse
	FALSE,			// fLimitEnabled
	0,				// wTimeLimit
	FALSE,			// fMCIControl
	FALSE,			// fStepMCIDevice
	0,0,			// dwMCIStartTime, dwMCIStopTime
	FALSE,			// fStepCaptureAt2x
	10,				// wStepCaptureAverageFrames
	0,				// dwAudioBufferSize
	FALSE,			// fDisableWriteCache
	AVSTREAMMASTER_NONE,	//	AVStreamMaster
};

class VDCaptureDriverVFW : public IVDCaptureDriver {
	VDCaptureDriverVFW(const VDCaptureDriverVFW&);
	VDCaptureDriverVFW& operator=(const VDCaptureDriverVFW&);
public:
	VDCaptureDriverVFW(HMODULE hmodAVICap, int driverIndex);
	~VDCaptureDriverVFW();

	void	*AsInterface(uint32 id) { return NULL; }

	bool	Init(VDGUIHandle hParent);
	void	Shutdown();

	void	SetCallback(IVDCaptureDriverCallback *pCB);

	void	LockUpdates() {}
	void	UnlockUpdates() {}

	bool	IsHardwareDisplayAvailable();

	void	SetDisplayMode(nsVDCapture::DisplayMode m);
	nsVDCapture::DisplayMode		GetDisplayMode();

	void	SetDisplayRect(const vdrect32& r);
	vdrect32	GetDisplayRectAbsolute();
	void	SetDisplayVisibility(bool vis);

	void	SetFramePeriod(sint32 ms);
	sint32	GetFramePeriod();

	uint32	GetPreviewFrameCount();

	bool	GetVideoFormat(vdstructex<BITMAPINFOHEADER>& vformat);
	bool	SetVideoFormat(const BITMAPINFOHEADER *pbih, uint32 size);

	bool	SetTunerChannel(int channel) { return false; }
	int		GetTunerChannel() { return -1; }
	bool	GetTunerChannelRange(int& minChannel, int& maxChannel) { return false; }
	uint32	GetTunerFrequencyPrecision() { return 0; }
	uint32	GetTunerExactFrequency() { return 0; }
	bool	SetTunerExactFrequency(uint32 freq) { return false; }
	nsVDCapture::TunerInputMode	GetTunerInputMode() { return kTunerInputUnknown; }
	void	SetTunerInputMode(nsVDCapture::TunerInputMode tunerMode) {}

	int		GetAudioDeviceCount();
	const wchar_t *GetAudioDeviceName(int idx);
	bool	SetAudioDevice(int idx);
	int		GetAudioDeviceIndex();
	bool	IsAudioDeviceIntegrated(int idx) { return false; }

	int		GetVideoSourceCount();
	const wchar_t *GetVideoSourceName(int idx);
	bool	SetVideoSource(int idx);
	int		GetVideoSourceIndex();

	int		GetAudioSourceCount();
	const wchar_t *GetAudioSourceName(int idx);
	bool	SetAudioSource(int idx);
	int		GetAudioSourceIndex();

	int		GetAudioInputCount();
	const wchar_t *GetAudioInputName(int idx);
	bool	SetAudioInput(int idx);
	int		GetAudioInputIndex();

	int		GetAudioSourceForVideoSource(int idx) { return -2; }

	bool	IsAudioCapturePossible();
	bool	IsAudioCaptureEnabled();
	bool	IsAudioPlaybackPossible() { return false; }
	bool	IsAudioPlaybackEnabled() { return false; }
	void	SetAudioCaptureEnabled(bool b);
	void	SetAudioAnalysisEnabled(bool b);
	void	SetAudioPlaybackEnabled(bool b) {}

	void	GetAvailableAudioFormats(std::list<vdstructex<WAVEFORMATEX> >& aformats);

	bool	GetAudioFormat(vdstructex<WAVEFORMATEX>& aformat);
	bool	SetAudioFormat(const WAVEFORMATEX *pwfex, uint32 size);

	bool	IsDriverDialogSupported(nsVDCapture::DriverDialog dlg);
	void	DisplayDriverDialog(nsVDCapture::DriverDialog dlg);

	bool	IsPropertySupported(uint32 id) { return false; }
	sint32	GetPropertyInt(uint32 id, bool *pAutomatic) { if (pAutomatic) *pAutomatic = true; return 0; }
	void	SetPropertyInt(uint32 id, sint32 value, bool automatic) {}
	void	GetPropertyInfoInt(uint32 id, sint32& minVal, sint32& maxVal, sint32& step, sint32& defaultVal, bool& automatic, bool& manual) {}

	bool	CaptureStart();
	void	CaptureStop();
	void	CaptureAbort();

protected:
	void	SyncCaptureStop();
	void	SyncCaptureAbort();
	void	InitMixerSupport();
	void	ShutdownMixerSupport();
	bool	InitWaveAnalysis();
	void	ShutdownWaveAnalysis();
	sint64	ComputeGlobalTime();

	static LRESULT CALLBACK ErrorCallback(HWND hWnd, int nID, LPCSTR lpsz);
	static LRESULT CALLBACK StatusCallback(HWND hWnd, int nID, LPCSTR lpsz);
	static LRESULT CALLBACK ControlCallback(HWND hwnd, int nState);
	static LRESULT CALLBACK PreviewCallback(HWND hWnd, VIDEOHDR *lpVHdr);
	static LRESULT CALLBACK VideoCallback(HWND hWnd, LPVIDEOHDR lpVHdr);
	static LRESULT CALLBACK WaveCallback(HWND hWnd, LPWAVEHDR lpWHdr);
	static LRESULT CALLBACK StaticMessageSinkWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);

	enum { kPreviewTimerID = 100 };

	HWND	mhwnd;
	HWND	mhwndParent;
	HWND	mhwndEventSink;
	HMODULE	mhmodAVICap;

	bool	mbCapturing;
	bool	mbVisible;
	bool	mbBlockVideoFrames;			///< Prevents video frames from being sent the callback. This is used to barrier frames while we sent video format change events.
	bool	mbAudioHardwarePresent;
	bool	mbAudioHardwareEnabled;
	bool	mbAudioCaptureEnabled;
	bool	mbAudioAnalysisEnabled;
	bool	mbAudioAnalysisActive;

	IVDCaptureDriverCallback	*mpCB;
	DisplayMode			mDisplayMode;
	uint32	mGlobalTimeBase;

	int		mDriverIndex;
	UINT	mPreviewFrameTimer;
	VDAtomicInt	mPreviewFrameCount;

	HMIXER	mhMixer;
	int		mMixerInput;
	MIXERCONTROL	mMixerInputControl;
	typedef std::vector<VDStringW>	MixerInputs;
	MixerInputs	mMixerInputs;

	HWAVEIN mhWaveIn;
	WAVEHDR	mWaveBufHdrs[2];
	vdblock<char>	mWaveBuffer;

	CAPDRIVERCAPS		mCaps;

	MyError	mCaptureError;

	static ATOM sMsgSinkClass;
};

ATOM VDCaptureDriverVFW::sMsgSinkClass;

VDCaptureDriverVFW::VDCaptureDriverVFW(HMODULE hmodAVICap, int driverIndex)
	: mhwnd(NULL)
	, mhwndParent(NULL)
	, mhwndEventSink(NULL)
	, mhmodAVICap(hmodAVICap)
	, mpCB(NULL)
	, mbCapturing(false)
	, mbVisible(false)
	, mbBlockVideoFrames(false)
	, mbAudioAnalysisEnabled(false)
	, mbAudioAnalysisActive(false)
	, mDisplayMode(kDisplayNone)
	, mDriverIndex(driverIndex)
	, mPreviewFrameTimer(0)
	, mhMixer(NULL)
	, mhWaveIn(NULL)
{
	memset(mWaveBufHdrs, 0, sizeof mWaveBufHdrs);
}

VDCaptureDriverVFW::~VDCaptureDriverVFW() {
	Shutdown();
}

void VDCaptureDriverVFW::SetCallback(IVDCaptureDriverCallback *pCB) {
	mpCB = pCB;
}

bool VDCaptureDriverVFW::Init(VDGUIHandle hParent) {
	mhwndParent = (HWND)hParent;

	if (!sMsgSinkClass) {
		WNDCLASS wc = { 0, StaticMessageSinkWndProc, 0, sizeof(VDCaptureDriverVFW *), g_hInst, NULL, NULL, NULL, NULL, "Riza VFW event sink" };

		sMsgSinkClass = RegisterClass(&wc);

		if (!sMsgSinkClass)
			return false;
	}

	// Attempt to open mixer device. It is OK for this to fail. Note that we
	// have a bit of a problem in that (a) the mixer API doesn't take
	// WAVE_MAPPER, and (b) we can't get access to the handle that the
	// capture window creates. For now, we sort of fake it.
	InitMixerSupport();

	// Create message sink.
	if (!(mhwndEventSink = CreateWindow((LPCTSTR)sMsgSinkClass, "", WS_POPUP, 0, 0, 0, 0, mhwndParent, NULL, g_hInst, this))) {
		Shutdown();
		return false;
	}

	typedef HWND (VFWAPI *tpcapCreateCaptureWindow)(LPCSTR lpszWindowName, DWORD dwStyle, int x, int y, int nWidth, int nHeight, HWND hwnd, int nID);
	const tpcapCreateCaptureWindow pcapCreateCaptureWindow = (tpcapCreateCaptureWindow)GetProcAddress(mhmodAVICap, "capCreateCaptureWindowA");
	if (!VDINLINEASSERT(pcapCreateCaptureWindow)) {
		Shutdown();
		return false;
	}

	mhwnd = pcapCreateCaptureWindow("", WS_CHILD, 0, 0, 0, 0, mhwndParent, 0);
	if (!mhwnd) {
		Shutdown();
		return false;
	}

	if (!capDriverConnect(mhwnd, mDriverIndex)) {
		Shutdown();
		return false;
	}

	VDVERIFY(capDriverGetCaps(mhwnd, &mCaps, sizeof mCaps));

	capCaptureSetSetup(mhwnd, &g_defaultCaptureParms, sizeof g_defaultCaptureParms);

	mPreviewFrameCount = 0;

	capSetUserData(mhwnd, this);
	capSetCallbackOnError(mhwnd, ErrorCallback);
	capSetCallbackOnStatus(mhwnd, StatusCallback);
	capSetCallbackOnCapControl(mhwnd, ControlCallback);
	capSetCallbackOnVideoStream(mhwnd, VideoCallback);
	capSetCallbackOnWaveStream(mhwnd, WaveCallback);

	capPreviewRate(mhwnd, 1000 / 15);

	CAPSTATUS cs;

	mbAudioHardwarePresent = false;
	if (VDINLINEASSERT(capGetStatus(mhwnd, &cs, sizeof(CAPSTATUS)))) {
		mbAudioHardwarePresent = (cs.fAudioHardware != 0);
	}
	mbAudioHardwareEnabled = mbAudioHardwarePresent;
	mbAudioCaptureEnabled = true;

	return true;
}

void VDCaptureDriverVFW::Shutdown() {
	ShutdownWaveAnalysis();

	if (mPreviewFrameTimer) {
		KillTimer(mhwndEventSink, mPreviewFrameTimer);
		mPreviewFrameTimer = 0;
	}

	if (mhwndEventSink) {
		DestroyWindow(mhwndEventSink);
		mhwndEventSink = NULL;
	}

	if (mhwnd) {
		capDriverDisconnect(mhwnd);
		DestroyWindow(mhwnd);
		mhwnd = NULL;
	}

	if (mhMixer) {
		mixerClose(mhMixer);
		mhMixer = NULL;
	}
}

bool VDCaptureDriverVFW::IsHardwareDisplayAvailable() {
	return 0 != mCaps.fHasOverlay;
}

void VDCaptureDriverVFW::SetDisplayMode(DisplayMode mode) {
	if (mode == mDisplayMode)
		return;

	if (mDisplayMode == kDisplayAnalyze) {
		if (mPreviewFrameTimer) {
			KillTimer(mhwndEventSink, mPreviewFrameTimer);
			mPreviewFrameTimer = 0;
		}
	}

	mDisplayMode = mode;

	switch(mode) {
	case kDisplayNone:
		capSetCallbackOnFrame(mhwnd, NULL);
		capPreview(mhwnd, FALSE);
		capOverlay(mhwnd, FALSE);
		ShowWindow(mhwnd, SW_HIDE);
		break;
	case kDisplayHardware:
		// we have to kill this callback or dual-field overlay may not work
		capSetCallbackOnFrame(mhwnd, NULL);
		capPreview(mhwnd, FALSE);
		capOverlay(mhwnd, TRUE);
		if (mbVisible)
			ShowWindow(mhwnd, SW_SHOWNA);
		break;
	case kDisplaySoftware:
		capSetCallbackOnFrame(mhwnd, PreviewCallback);
		capOverlay(mhwnd, FALSE);
		capPreview(mhwnd, TRUE);
		if (mbVisible)
			ShowWindow(mhwnd, SW_SHOWNA);
		break;
	case kDisplayAnalyze:
		capSetCallbackOnFrame(mhwnd, PreviewCallback);
		capOverlay(mhwnd, FALSE);
		capPreview(mhwnd, FALSE);
		if (!mPreviewFrameTimer)
			mPreviewFrameTimer = SetTimer(mhwndEventSink, kPreviewTimerID, 66, NULL);
		if (mbVisible)
			ShowWindow(mhwnd, SW_HIDE);
		break;
	}
}

DisplayMode VDCaptureDriverVFW::GetDisplayMode() {
	return mDisplayMode;
}

void VDCaptureDriverVFW::SetDisplayRect(const vdrect32& r) {
	SetWindowPos(mhwnd, NULL, r.left, r.top, r.width(), r.height(), SWP_NOZORDER|SWP_NOACTIVATE);
}

vdrect32 VDCaptureDriverVFW::GetDisplayRectAbsolute() {
	RECT r;
	GetWindowRect(mhwnd, &r);
	MapWindowPoints(GetParent(mhwnd), NULL, (LPPOINT)&r, 2);
	return vdrect32(r.left, r.top, r.right, r.bottom);
}

void VDCaptureDriverVFW::SetDisplayVisibility(bool vis) {
	if (vis == mbVisible)
		return;

	mbVisible = vis;
	ShowWindow(mhwnd, vis && mDisplayMode != kDisplayAnalyze ? SW_SHOWNA : SW_HIDE);
}

void VDCaptureDriverVFW::SetFramePeriod(sint32 framePeriod100nsUnits) {
	CAPTUREPARMS cp;

	if (capCaptureGetSetup(mhwnd, &cp, sizeof(CAPTUREPARMS))) {
		cp.dwRequestMicroSecPerFrame = (framePeriod100nsUnits + 5) / 10;

		capCaptureSetSetup(mhwnd, &cp, sizeof(CAPTUREPARMS));

		if (mpCB)
			mpCB->CapEvent(kEventVideoFrameRateChanged, 0);
	}
}

sint32 VDCaptureDriverVFW::GetFramePeriod() {
	CAPTUREPARMS cp;

	if (VDINLINEASSERT(capCaptureGetSetup(mhwnd, &cp, sizeof(CAPTUREPARMS))))
		return cp.dwRequestMicroSecPerFrame*10;

	return 10000000 / 15;
}

uint32 VDCaptureDriverVFW::GetPreviewFrameCount() {
	if (mDisplayMode == kDisplaySoftware || mDisplayMode == kDisplayAnalyze)
		return mPreviewFrameCount;

	return 0;
}

bool VDCaptureDriverVFW::GetVideoFormat(vdstructex<BITMAPINFOHEADER>& vformat) {
	DWORD dwSize = capGetVideoFormatSize(mhwnd);

	vformat.resize(dwSize);

	VDVERIFY(capGetVideoFormat(mhwnd, vformat.data(), vformat.size()));
	return true;
}

bool VDCaptureDriverVFW::SetVideoFormat(const BITMAPINFOHEADER *pbih, uint32 size) {
	bool success = false;

	mbBlockVideoFrames = true;
	if (capSetVideoFormat(mhwnd, (BITMAPINFOHEADER *)pbih, size)) {
		if (mpCB)
			mpCB->CapEvent(kEventVideoFormatChanged, 0);
		success = true;
	}
	mbBlockVideoFrames = false;
	return success;
}

int VDCaptureDriverVFW::GetAudioDeviceCount() {
	return mbAudioHardwarePresent ? 1 : 0;
}

const wchar_t *VDCaptureDriverVFW::GetAudioDeviceName(int idx) {
	if (idx || !mbAudioHardwarePresent)
		return NULL;

	return L"Wave Mapper";
}

bool VDCaptureDriverVFW::SetAudioDevice(int idx) {
	if (idx < -1 || idx >= 1)
		return false;
	
	if (!idx && !mbAudioHardwarePresent)
		return false;

	bool enable = !idx;

	if (enable == mbAudioHardwareEnabled)
		return true;

	ShutdownWaveAnalysis();
	mbAudioHardwareEnabled = enable;
	InitWaveAnalysis();

	return true;
}

int VDCaptureDriverVFW::GetAudioDeviceIndex() {
	return mbAudioHardwareEnabled ? 0 : -1;
}

int VDCaptureDriverVFW::GetVideoSourceCount() {
	return 0;
}

const wchar_t *VDCaptureDriverVFW::GetVideoSourceName(int idx) {
	return NULL;
}

bool VDCaptureDriverVFW::SetVideoSource(int idx) {
	return idx == -1;
}

int VDCaptureDriverVFW::GetVideoSourceIndex() {
	return -1;
}

int VDCaptureDriverVFW::GetAudioSourceCount() {
	return 0;
}

const wchar_t *VDCaptureDriverVFW::GetAudioSourceName(int idx) {
	return NULL;
}

bool VDCaptureDriverVFW::SetAudioSource(int idx) {
	return idx == -1;
}

int VDCaptureDriverVFW::GetAudioSourceIndex() {
	return -1;
}

int VDCaptureDriverVFW::GetAudioInputCount() {
	return mbAudioHardwareEnabled ? mMixerInputs.size() : 0;
}

const wchar_t *VDCaptureDriverVFW::GetAudioInputName(int idx) {
	if (!mbAudioHardwareEnabled || (unsigned)idx >= mMixerInputs.size())
		return NULL;

	MixerInputs::const_iterator it(mMixerInputs.begin());

	std::advance(it, idx);

	return (*it).c_str();
}

bool VDCaptureDriverVFW::SetAudioInput(int idx) {
	if (!mbAudioHardwareEnabled || !mhMixer)
		return idx == -1;

	VDASSERT(mMixerInputs.size() == mMixerInputControl.cMultipleItems);

	if (idx != -1 && (unsigned)idx >= mMixerInputControl.cMultipleItems)
		return false;

	// attempt to set the appropriate mixer input

	vdblock<MIXERCONTROLDETAILS_BOOLEAN> vals(mMixerInputControl.cMultipleItems);

	for(unsigned i=0; i<mMixerInputControl.cMultipleItems; ++i)
		vals[i].fValue = (i == idx);

	MIXERCONTROLDETAILS details = {sizeof(MIXERCONTROLDETAILS)};

	details.dwControlID		= mMixerInputControl.dwControlID;
	details.cChannels		= 1;
	details.cMultipleItems	= mMixerInputControl.cMultipleItems;
	details.cbDetails		= sizeof(MIXERCONTROLDETAILS_BOOLEAN);
	details.paDetails		= vals.data();

	if (MMSYSERR_NOERROR != mixerSetControlDetails((HMIXEROBJ)mhMixer, &details, MIXER_SETCONTROLDETAILSF_VALUE))
		return false;

	mMixerInput = idx;

	return true;
}

int VDCaptureDriverVFW::GetAudioInputIndex() {
	return mbAudioHardwareEnabled ? mMixerInput : -1;
}

bool VDCaptureDriverVFW::IsAudioCapturePossible() {
	return mbAudioHardwareEnabled;
}

bool VDCaptureDriverVFW::IsAudioCaptureEnabled() {
	return mbAudioHardwareEnabled && mbAudioCaptureEnabled;
}

void VDCaptureDriverVFW::SetAudioCaptureEnabled(bool b) {
	mbAudioCaptureEnabled = b;
}

void VDCaptureDriverVFW::SetAudioAnalysisEnabled(bool b) {
	if (mbAudioAnalysisEnabled == b)
		return;

	mbAudioAnalysisEnabled = b;

	if (mbAudioAnalysisEnabled)
		InitWaveAnalysis();
	else
		ShutdownWaveAnalysis();
}

void VDCaptureDriverVFW::GetAvailableAudioFormats(std::list<vdstructex<WAVEFORMATEX> >& aformats) {
	static const int kSamplingRates[]={
		8000,
		11025,
		12000,
		16000,
		22050,
		24000,
		32000,
		44100,
		48000,
		96000,
		192000
	};

	static const int kChannelCounts[]={
		1,
		2
	};

	static const int kSampleDepths[]={
		8,
		16
	};

	for(int sridx=0; sridx < sizeof kSamplingRates / sizeof kSamplingRates[0]; ++sridx)
		for(int chidx=0; chidx < sizeof kChannelCounts / sizeof kChannelCounts[0]; ++chidx)
			for(int sdidx=0; sdidx < sizeof kSampleDepths / sizeof kSampleDepths[0]; ++sdidx) {
				WAVEFORMATEX wfex={
					WAVE_FORMAT_PCM,
					kChannelCounts[chidx],
					kSamplingRates[sridx],
					0,
					0,
					kSampleDepths[sdidx],
					0
				};

				wfex.nBlockAlign = wfex.nChannels * (wfex.wBitsPerSample >> 3);
				wfex.nAvgBytesPerSec = wfex.nBlockAlign * wfex.nSamplesPerSec;

				if (MMSYSERR_NOERROR ==waveInOpen(NULL, WAVE_MAPPER, &wfex, 0, 0, WAVE_FORMAT_QUERY | WAVE_FORMAT_DIRECT)) {
					aformats.push_back(vdstructex<WAVEFORMATEX>(&wfex, sizeof wfex));
				}
			}
}

bool VDCaptureDriverVFW::GetAudioFormat(vdstructex<WAVEFORMATEX>& aformat) {
	DWORD dwSize = capGetAudioFormatSize(mhwnd);

	if (!dwSize)
		return false;

	aformat.resize(dwSize);

	if (dwSize < sizeof(PCMWAVEFORMAT))
		return false;

	if (!capGetAudioFormat(mhwnd, aformat.data(), aformat.size()))
		return false;

	// adjust PCMWAVEFORMAT to WAVEFORMATEX
	if (dwSize < sizeof(WAVEFORMATEX)) {
		aformat.resize(sizeof(WAVEFORMATEX));

		aformat->cbSize = 0;
	}

	return true;
}

bool VDCaptureDriverVFW::SetAudioFormat(const WAVEFORMATEX *pwfex, uint32 size) {
	if (!mbAudioHardwareEnabled)
		return false;

	ShutdownWaveAnalysis();
	bool success = 0 != capSetAudioFormat(mhwnd, (WAVEFORMATEX *)pwfex, size);
	if (mbAudioAnalysisEnabled)
		InitWaveAnalysis();
	return success;
}

bool VDCaptureDriverVFW::IsDriverDialogSupported(DriverDialog dlg) {
	switch(dlg) {
	case kDialogVideoFormat:
		return mCaps.fHasDlgVideoFormat != 0;
	case kDialogVideoSource:
		return mCaps.fHasDlgVideoSource != 0;
	case kDialogVideoDisplay:
		return mCaps.fHasDlgVideoDisplay != 0;
	}

	return false;
}

void VDCaptureDriverVFW::DisplayDriverDialog(DriverDialog dlg) {
	VDASSERT(IsDriverDialogSupported(dlg));

	switch(dlg) {
	case kDialogVideoFormat:
		// The format dialog is special (i.e. nasty) in that it can change the
		// video format, so we block outgoing frames while it is up and check
		// for a format change.
		{
			vdstructex<BITMAPINFOHEADER> oldFormat, newFormat;

			mbBlockVideoFrames = true;
			GetVideoFormat(oldFormat);
			capDlgVideoFormat(mhwnd);
			GetVideoFormat(newFormat);
			if (oldFormat != newFormat && mpCB)
				mpCB->CapEvent(kEventVideoFormatChanged, 0);
			mbBlockVideoFrames = false;
		}
		break;
	case kDialogVideoSource:
		capDlgVideoSource(mhwnd);
		break;
	case kDialogVideoDisplay:
		capDlgVideoDisplay(mhwnd);
		break;
	}
}

bool VDCaptureDriverVFW::CaptureStart() {
	ShutdownWaveAnalysis();

	// Aim for 0.1s audio buffers.
	CAPTUREPARMS cp;
	if (capCaptureGetSetup(mhwnd, &cp, sizeof(CAPTUREPARMS))) {
		cp.fCaptureAudio = mbAudioHardwareEnabled && mbAudioCaptureEnabled;

		if (cp.fCaptureAudio) {
			vdstructex<WAVEFORMATEX> wfex;
			if (GetAudioFormat(wfex)) {
				cp.wNumAudioRequested = 10;
				cp.dwAudioBufferSize = (wfex->nAvgBytesPerSec / 10 + wfex->nBlockAlign - 1);
				cp.dwAudioBufferSize -= cp.dwAudioBufferSize % wfex->nBlockAlign;
			}
		}

		capCaptureSetSetup(mhwnd, &cp, sizeof(CAPTUREPARMS));
	}

	if (!VDINLINEASSERTFALSE(mbCapturing)) {
		mGlobalTimeBase = VDGetAccurateTick();
		mbCapturing = !!capCaptureSequenceNoFile(mhwnd);

		if (!mbCapturing && mbAudioAnalysisEnabled)
			InitWaveAnalysis();
	}

	return mbCapturing;
}

void VDCaptureDriverVFW::CaptureStop() {
	SendMessage(mhwndEventSink, WM_APP+16, 0, 0);
}

void VDCaptureDriverVFW::CaptureAbort() {
	SendMessage(mhwndEventSink, WM_APP+17, 0, 0);
}

void VDCaptureDriverVFW::SyncCaptureStop() {
	if (VDINLINEASSERT(mbCapturing)) {
		capCaptureStop(mhwnd);
		mbCapturing = false;

		if (mbAudioAnalysisEnabled)
			InitWaveAnalysis();
	}
}

void VDCaptureDriverVFW::SyncCaptureAbort() {
	if (mbCapturing) {
		capCaptureAbort(mhwnd);
		mbCapturing = false;

		if (mbAudioAnalysisEnabled)
			InitWaveAnalysis();
	}
}

void VDCaptureDriverVFW::InitMixerSupport() {
	WAVEINCAPS wcaps={0};
	if (MMSYSERR_NOERROR == waveInGetDevCaps(WAVE_MAPPER, &wcaps, sizeof wcaps) && wcaps.dwFormats) {
		WAVEFORMATEX wfex;

		// create lowest-common denominator format for device
		wfex.wFormatTag			= WAVE_FORMAT_PCM;

		if (wcaps.dwFormats & (WAVE_FORMAT_4M08 | WAVE_FORMAT_4M16 | WAVE_FORMAT_4S08 | WAVE_FORMAT_4S16))
			wfex.nSamplesPerSec = 11025;
		else if (wcaps.dwFormats & (WAVE_FORMAT_2M08 | WAVE_FORMAT_2M16 | WAVE_FORMAT_2S08 | WAVE_FORMAT_2S16))
			wfex.nSamplesPerSec = 22050;
		else
			wfex.nSamplesPerSec = 44100;

		if (wcaps.dwFormats & (WAVE_FORMAT_1M08 | WAVE_FORMAT_1M16 | WAVE_FORMAT_2M08 | WAVE_FORMAT_2M16 | WAVE_FORMAT_4M08 | WAVE_FORMAT_4M16))
			wfex.nChannels = 1;
		else
			wfex.nChannels = 2;

		if (wcaps.dwFormats & (WAVE_FORMAT_1M08 | WAVE_FORMAT_1S08 | WAVE_FORMAT_2M08 | WAVE_FORMAT_2S08 | WAVE_FORMAT_4M08 | WAVE_FORMAT_4S08))
			wfex.wBitsPerSample = 8;
		else
			wfex.wBitsPerSample = 16;

		wfex.nBlockAlign		= wfex.wBitsPerSample >> 3;
		wfex.nAvgBytesPerSec	= wfex.nSamplesPerSec * wfex.nBlockAlign;
		wfex.cbSize				= 0;

		// create the device
		HWAVEIN hwi;
		if (MMSYSERR_NOERROR == waveInOpen(&hwi, WAVE_MAPPER, &wfex, 0, 0, CALLBACK_NULL)) {
			// create mixer based on device

			if (MMSYSERR_NOERROR == mixerOpen(&mhMixer, (UINT)hwi, 0, 0, MIXER_OBJECTF_HWAVEIN)) {
				MIXERLINE mixerLine = {sizeof(MIXERLINE)};

				mixerLine.dwComponentType = MIXERLINE_COMPONENTTYPE_DST_WAVEIN;

				if (MMSYSERR_NOERROR == mixerGetLineInfo((HMIXEROBJ)mhMixer, &mixerLine, MIXER_GETLINEINFOF_COMPONENTTYPE)) {

					// Try to find a MIXER or MUX control
					MIXERLINECONTROLS lineControls = {sizeof(MIXERLINECONTROLS)};

					mMixerInputControl.cbStruct = sizeof(MIXERCONTROL);
					mMixerInputControl.dwControlType = 0;

					lineControls.dwLineID = mixerLine.dwLineID;
					lineControls.dwControlType = MIXERCONTROL_CONTROLTYPE_MUX;
					lineControls.cControls = 1;
					lineControls.pamxctrl = &mMixerInputControl;
					lineControls.cbmxctrl = sizeof(MIXERCONTROL);

					MMRESULT res;

					res = mixerGetLineControls((HMIXEROBJ)mhMixer, &lineControls, MIXER_GETLINECONTROLSF_ONEBYTYPE);

					if (MMSYSERR_NOERROR != res) {
						lineControls.dwControlType = MIXERCONTROL_CONTROLTYPE_MIXER;

						res = mixerGetLineControls((HMIXEROBJ)mhMixer, &lineControls, MIXER_GETLINECONTROLSF_ONEBYTYPE);
					}

					// The mux/mixer control must be of MULTIPLE type; otherwise, we reject it.
					if (!(mMixerInputControl.fdwControl & MIXERCONTROL_CONTROLF_MULTIPLE))
						res = MMSYSERR_ERROR;

					// If we were successful, then enumerate all source lines and push them into the map.
					if (MMSYSERR_NOERROR != res) {
						mixerClose(mhMixer);
						mhMixer = NULL;
					} else {
						// Enumerate control inputs and populate the name array
						vdblock<MIXERCONTROLDETAILS_LISTTEXT> names(mMixerInputControl.cMultipleItems);

						MIXERCONTROLDETAILS details = {sizeof(MIXERCONTROLDETAILS)};

						details.dwControlID		= mMixerInputControl.dwControlID;
						details.cChannels		= 1;
						details.cMultipleItems	= mMixerInputControl.cMultipleItems;
						details.cbDetails		= sizeof(MIXERCONTROLDETAILS_LISTTEXT);
						details.paDetails		= names.data();

						mMixerInput = -1;

						if (MMSYSERR_NOERROR == mixerGetControlDetails((HMIXEROBJ)mhMixer, &details, MIXER_GETCONTROLDETAILSF_LISTTEXT)) {
							mMixerInputs.reserve(details.cMultipleItems);

							for(unsigned i=0; i<details.cMultipleItems; ++i)
								mMixerInputs.push_back(MixerInputs::value_type(VDTextAToW(names[i].szName)));

							vdblock<MIXERCONTROLDETAILS_BOOLEAN> vals(mMixerInputControl.cMultipleItems);

							details.cbDetails = sizeof(MIXERCONTROLDETAILS_BOOLEAN);
							details.paDetails = vals.data();

							if (MMSYSERR_NOERROR == mixerGetControlDetails((HMIXEROBJ)mhMixer, &details, MIXER_GETCONTROLDETAILSF_VALUE)) {
								// Attempt to find a mixer input that is set. Note that for
								// a multiple-select type (MIXER) this will pick the first
								// enabled input.
								for(unsigned i=0; i<details.cMultipleItems; ++i)
									if (vals[i].fValue) {
										mMixerInput = i;
										break;
									}
							}
						}
					}
				}

				// We don't close the mixer here; it is left open while we have the
				// capture device opened.
			}

			waveInClose(hwi);
		}
	}
}

void VDCaptureDriverVFW::ShutdownMixerSupport() {
	if (mhMixer) {
		mixerClose(mhMixer);
		mhMixer = NULL;
	}
}

bool VDCaptureDriverVFW::InitWaveAnalysis() {
	if (!mbAudioHardwareEnabled)
		return false;

	vdstructex<WAVEFORMATEX> aformat;

	if (!GetAudioFormat(aformat))
		return false;

	uint32	blockSize = (aformat->nAvgBytesPerSec + 9) / 10 + aformat->nBlockAlign - 1;
	blockSize -= blockSize % aformat->nBlockAlign;

	mWaveBuffer.resize(blockSize*2);

	if (MMSYSERR_NOERROR != waveInOpen(&mhWaveIn, WAVE_MAPPER, aformat.data(), (DWORD_PTR)mhwndEventSink, 0, CALLBACK_WINDOW | WAVE_FORMAT_DIRECT))
		return false;

	mbAudioAnalysisActive = true;
	for(int i=0; i<2; ++i) {
		WAVEHDR& hdr = mWaveBufHdrs[i];

		hdr.lpData			= &mWaveBuffer[blockSize*i];
		hdr.dwBufferLength	= blockSize;
		hdr.dwBytesRecorded	= 0;
		hdr.dwFlags			= 0;
		hdr.dwLoops			= 0;
		if (MMSYSERR_NOERROR != waveInPrepareHeader(mhWaveIn, &hdr, sizeof(WAVEHDR))) {
			ShutdownWaveAnalysis();
			return false;
		}

		if (MMSYSERR_NOERROR != waveInAddBuffer(mhWaveIn, &hdr, sizeof(WAVEHDR))) {
			ShutdownWaveAnalysis();
			return false;
		}
	}

	if (MMSYSERR_NOERROR != waveInStart(mhWaveIn)) {
		ShutdownWaveAnalysis();
		return false;
	}

	return true;
}

void VDCaptureDriverVFW::ShutdownWaveAnalysis() {
	if (mhWaveIn) {
		mbAudioAnalysisActive = false;
		waveInReset(mhWaveIn);

		for(int i=0; i<2; ++i) {
			if (mWaveBufHdrs[i].dwFlags & WHDR_PREPARED)
				waveInUnprepareHeader(mhWaveIn, &mWaveBufHdrs[i], sizeof(WAVEHDR));
		}

		waveInClose(mhWaveIn);
		mhWaveIn = NULL;
	}

	mWaveBuffer.clear();
}

sint64 VDCaptureDriverVFW::ComputeGlobalTime() {
	// AVICap internally seems to use timeGetTime() when queried via capGetStatus(), but
	// this isn't guaranteed anywhere.
	return (sint64)((uint64)(VDGetAccurateTick() - mGlobalTimeBase) * 1000);
}

#pragma vdpragma_TODO("Need to find some way to propagate these errors back nicely")

LRESULT CALLBACK VDCaptureDriverVFW::ErrorCallback(HWND hwnd, int nID, LPCSTR lpsz) {
#if 0
	static const struct {
		int id;
		const char *szError;
	} g_betterCaptureErrors[]={
		{ 434,	"Warning: No frames captured.\n"
				"\n"
				"Make sure your capture card is functioning correctly and that a valid video source "
				"is connected.  You might also try turning off overlay, reducing the image size, or "
				"reducing the image depth to 24 or 16-bit." },
		{ 439,	"Error: Cannot find a driver to draw this non-RGB image format.  Preview and histogram functions will be unavailable." },
	};

	char buf[1024];
	int i;

	if (!nID) return 0;

	for(i=0; i<sizeof g_betterCaptureErrors/sizeof g_betterCaptureErrors[0]; i++)
		if (g_betterCaptureErrors[i].id == nID) {
			MessageBox(GetParent(hWnd), g_betterCaptureErrors[i].szError, "VirtualDub capture error", MB_OK);
			return 0;
		}

	buf[0] = 0;
	_snprintf(buf, sizeof buf / sizeof buf[0], "Error %d: %s", nID, lpsz);
	MessageBox(GetParent(hWnd), buf, "VirtualDub capture error", MB_OK);
	VDDEBUG("%s\n",buf);
#endif

	return 0;
}

LRESULT CALLBACK VDCaptureDriverVFW::StatusCallback(HWND hwnd, int nID, LPCSTR lpsz) {
	VDCaptureDriverVFW *pThis = (VDCaptureDriverVFW *)capGetUserData(hwnd);

	if (nID == IDS_CAP_BEGIN) {
		CAPSTATUS capStatus;
		capGetStatus(hwnd, (LPARAM)&capStatus, sizeof(CAPSTATUS));

		if (pThis->mpCB) {
			try {
				pThis->mpCB->CapBegin((sint64)capStatus.dwCurrentTimeElapsedMS * 1000);
			} catch(MyError& e) {
				pThis->mCaptureError.TransferFrom(e);
				capCaptureAbort(hwnd);
				pThis->mbCapturing = false;
			}
		}
	} else if (nID == IDS_CAP_END) {
		if (pThis->mpCB) {
			pThis->mpCB->CapEnd(pThis->mCaptureError.gets() ? &pThis->mCaptureError : NULL);
			pThis->mbCapturing = false;
		}

		pThis->mCaptureError.discard();
	}

#if 0
	char buf[1024];

	// Intercept nID=510 (per frame info)

	if (nID == 510)
		return 0;

	if (nID) {
		buf[0] = 0;
		_snprintf(buf, sizeof buf / sizeof buf[0], "Status %d: %s", nID, lpsz);
		SendMessage(GetDlgItem(GetParent(hWnd), IDC_STATUS_WINDOW), SB_SETTEXT, 0, (LPARAM)buf);
		VDDEBUG("%s\n",buf);
	} else {
		SendMessage(GetDlgItem(GetParent(hWnd), IDC_STATUS_WINDOW), SB_SETTEXT, 0, (LPARAM)"");
	}

	return 0;
#endif

	return 0;
}

LRESULT CALLBACK VDCaptureDriverVFW::ControlCallback(HWND hwnd, int nState) {
	VDCaptureDriverVFW *pThis = (VDCaptureDriverVFW *)capGetUserData(hwnd);

	if (pThis->mpCB) {
		switch(nState) {
		case CONTROLCALLBACK_PREROLL:
			return pThis->mpCB->CapEvent(kEventPreroll, 0);
		case CONTROLCALLBACK_CAPTURING:
			return pThis->mpCB->CapEvent(kEventCapturing, 0);
		}
	}

	return TRUE;
}

LRESULT CALLBACK VDCaptureDriverVFW::PreviewCallback(HWND hwnd, VIDEOHDR *lpVHdr) {
	VDCaptureDriverVFW *pThis = (VDCaptureDriverVFW *)capGetUserData(hwnd);
	if (pThis->mpCB && pThis->mDisplayMode == kDisplayAnalyze && !pThis->mbBlockVideoFrames) {
		try {
			pThis->mpCB->CapProcessData(-1, lpVHdr->lpData, lpVHdr->dwBytesUsed, (sint64)lpVHdr->dwTimeCaptured * 1000, 0 != (lpVHdr->dwFlags & VHDR_KEYFRAME), 0);
		} catch(MyError&) {
			// Eat preview errors.
		}
	}
	++pThis->mPreviewFrameCount;
	return 0;
}

LRESULT CALLBACK VDCaptureDriverVFW::VideoCallback(HWND hwnd, LPVIDEOHDR lpVHdr) {
	VDCaptureDriverVFW *pThis = (VDCaptureDriverVFW *)capGetUserData(hwnd);

	if (pThis->mpCB && !pThis->mbBlockVideoFrames) {
		try {
			pThis->mpCB->CapProcessData(0, lpVHdr->lpData, lpVHdr->dwBytesUsed, (sint64)lpVHdr->dwTimeCaptured * 1000, 0 != (lpVHdr->dwFlags & VHDR_KEYFRAME), pThis->ComputeGlobalTime());
		} catch(MyError& e) {
			pThis->mCaptureError.TransferFrom(e);
			capCaptureAbort(hwnd);
			pThis->mbCapturing = false;
		}
	}
	return 0;
}

LRESULT CALLBACK VDCaptureDriverVFW::WaveCallback(HWND hwnd, LPWAVEHDR lpWHdr) {
	VDCaptureDriverVFW *pThis = (VDCaptureDriverVFW *)capGetUserData(hwnd);

	if (pThis->mpCB) {
		try {
			pThis->mpCB->CapProcessData(1, lpWHdr->lpData, lpWHdr->dwBytesRecorded, -1, false, pThis->ComputeGlobalTime());
		} catch(MyError& e) {
			pThis->mCaptureError.TransferFrom(e);
			capCaptureAbort(hwnd);
			pThis->mbCapturing = false;
		}
	}
	return 0;
}

LRESULT CALLBACK VDCaptureDriverVFW::StaticMessageSinkWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
	switch(msg) {
		case WM_NCCREATE:
			SetWindowLongPtr(hwnd, 0, (LONG_PTR)((LPCREATESTRUCT)lParam)->lpCreateParams);
			break;
		case MM_WIM_DATA:
			{
				VDCaptureDriverVFW *pThis = (VDCaptureDriverVFW *)GetWindowLongPtr(hwnd, 0);

				if (pThis->mpCB) {
					WAVEHDR& hdr = *(WAVEHDR *)lParam;

					if (pThis->mbAudioAnalysisActive) {
						// For some reason this is sometimes called after reset. Don't know why yet.
						if (hdr.dwBytesRecorded) {
							try {
								pThis->mpCB->CapProcessData(-2, hdr.lpData, hdr.dwBytesRecorded, -1, false, 0);
							} catch(const MyError&) {
								// eat the error
							}
						}

						waveInAddBuffer(pThis->mhWaveIn, &hdr, sizeof(WAVEHDR));
					}
				}
			}
			return 0;
		case WM_APP+16:
			{
				VDCaptureDriverVFW *pThis = (VDCaptureDriverVFW *)GetWindowLongPtr(hwnd, 0);
				pThis->SyncCaptureStop();
			}
			return 0;
		case WM_APP+17:
			{
				VDCaptureDriverVFW *pThis = (VDCaptureDriverVFW *)GetWindowLongPtr(hwnd, 0);
				pThis->SyncCaptureAbort();
			}
			return 0;
		case WM_TIMER:
			{
				VDCaptureDriverVFW *pThis = (VDCaptureDriverVFW *)GetWindowLongPtr(hwnd, 0);
				if (pThis->mDisplayMode == kDisplayAnalyze && !pThis->mbCapturing)
					capGrabFrameNoStop(pThis->mhwnd);
			}
			return 0;
	}

	return DefWindowProc(hwnd, msg, wParam, lParam);
}

///////////////////////////////////////////////////////////////////////////

class VDCaptureSystemVFW : public IVDCaptureSystem {
public:
	VDCaptureSystemVFW();
	~VDCaptureSystemVFW();

	void EnumerateDrivers();

	int GetDeviceCount();
	const wchar_t *GetDeviceName(int index);

	IVDCaptureDriver *CreateDriver(int deviceIndex);

protected:
	HMODULE	mhmodAVICap;
	int mDriverCount;
	VDStringW mDrivers[10];
};

IVDCaptureSystem *VDCreateCaptureSystemVFW() {
	return new VDCaptureSystemVFW;
}

VDCaptureSystemVFW::VDCaptureSystemVFW()
	: mhmodAVICap(LoadLibrary("avicap32"))
	, mDriverCount(0)
{
}

VDCaptureSystemVFW::~VDCaptureSystemVFW() {
	if (mhmodAVICap)
		FreeLibrary(mhmodAVICap);
}

void VDCaptureSystemVFW::EnumerateDrivers() {
	if (!mhmodAVICap)
		return;

	typedef BOOL (VFWAPI *tpcapGetDriverDescriptionA)(UINT wDriverIndex, LPSTR lpszName, INT cbName, LPSTR lpszVer, INT cbVer);

	const tpcapGetDriverDescriptionA pcapGetDriverDescriptionA = (tpcapGetDriverDescriptionA)GetProcAddress(mhmodAVICap, "capGetDriverDescriptionA");

	if (pcapGetDriverDescriptionA) {
		char buf[256], ver[256];

		mDriverCount = 0;
		while(mDriverCount < 10 && pcapGetDriverDescriptionA(mDriverCount, buf, sizeof buf, ver, sizeof ver)) {
			mDrivers[mDriverCount] = VDTextAToW(buf) + L" (VFW)";
			++mDriverCount;
		}
	}
}

int VDCaptureSystemVFW::GetDeviceCount() {
	return mDriverCount;
}

const wchar_t *VDCaptureSystemVFW::GetDeviceName(int index) {
	return (unsigned)index < (unsigned)mDriverCount ? mDrivers[index].c_str() : NULL;
}

IVDCaptureDriver *VDCaptureSystemVFW::CreateDriver(int index) {
	if ((unsigned)index >= (unsigned)mDriverCount)
		return NULL;

	return new VDCaptureDriverVFW(mhmodAVICap, index);
}
